@hypothesi/tauri-mcp-server 0.1.3 β†’ 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,7 @@ A **Model Context Protocol (MCP) server** that enables AI assistants like Claude
11
11
 
12
12
  | Category | Capabilities |
13
13
  |----------|-------------|
14
- | 🎯 **UI Automation** | Screenshots, clicks, typing, scrollingβ€”all via WebSocket |
14
+ | 🎯 **UI Automation** | Screenshots, clicks, typing, scrolling, element finding |
15
15
  | πŸ” **IPC Monitoring** | Capture and inspect Tauri IPC calls in real-time |
16
16
  | πŸ“± **Mobile Dev** | Manage Android emulators & iOS simulators |
17
17
  | πŸ› οΈ **CLI Integration** | Run any Tauri command (`init`, `dev`, `build`, etc.) |
@@ -140,3 +140,32 @@ export async function getBackendState() {
140
140
  throw new Error(`Failed to get backend state: ${message}`);
141
141
  }
142
142
  }
143
+ // ============================================================================
144
+ // Window Management
145
+ // ============================================================================
146
+ export const ListWindowsSchema = z.object({});
147
+ /**
148
+ * Lists all open webview windows in the Tauri application.
149
+ */
150
+ export async function listWindows() {
151
+ try {
152
+ await connectPlugin();
153
+ const client = getPluginClient();
154
+ const response = await client.sendCommand({
155
+ command: 'list_windows',
156
+ });
157
+ if (!response.success) {
158
+ throw new Error(response.error || 'Unknown error');
159
+ }
160
+ const windows = response.data;
161
+ return JSON.stringify({
162
+ windows,
163
+ defaultWindow: 'main',
164
+ totalCount: windows.length,
165
+ });
166
+ }
167
+ catch (error) {
168
+ const message = error instanceof Error ? error.message : String(error);
169
+ throw new Error(`Failed to list windows: ${message}`);
170
+ }
171
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Script Manager - Manages persistent script injection across page navigations.
3
+ *
4
+ * This module provides functions to register, remove, and manage scripts that
5
+ * should be automatically re-injected when pages load or navigate.
6
+ *
7
+ * @internal This module is for internal use only and is not exposed as MCP tools.
8
+ */
9
+ import { getPluginClient, connectPlugin } from './plugin-client.js';
10
+ /**
11
+ * Registers a script to be injected into the webview.
12
+ *
13
+ * The script will be immediately injected if the page is loaded, and will be
14
+ * automatically re-injected on subsequent page loads/navigations.
15
+ *
16
+ * @param id - Unique identifier for the script
17
+ * @param type - Type of script ('inline' for code, 'url' for external script)
18
+ * @param content - The script content (JavaScript code) or URL
19
+ * @param windowLabel - Optional window label to target
20
+ * @returns Promise resolving to registration result
21
+ */
22
+ export async function registerScript(id, type, content, windowLabel) {
23
+ await connectPlugin();
24
+ const client = getPluginClient();
25
+ const response = await client.sendCommand({
26
+ command: 'register_script',
27
+ args: { id, type, content, windowLabel },
28
+ });
29
+ if (!response.success) {
30
+ throw new Error(response.error || 'Failed to register script');
31
+ }
32
+ return response.data;
33
+ }
34
+ /**
35
+ * Removes a script from the registry and DOM.
36
+ *
37
+ * @param id - The script ID to remove
38
+ * @param windowLabel - Optional window label to target
39
+ * @returns Promise resolving to removal result
40
+ */
41
+ export async function removeScript(id, windowLabel) {
42
+ await connectPlugin();
43
+ const client = getPluginClient();
44
+ const response = await client.sendCommand({
45
+ command: 'remove_script',
46
+ args: { id, windowLabel },
47
+ });
48
+ if (!response.success) {
49
+ throw new Error(response.error || 'Failed to remove script');
50
+ }
51
+ return response.data;
52
+ }
53
+ /**
54
+ * Clears all registered scripts from the registry and DOM.
55
+ *
56
+ * @param windowLabel - Optional window label to target
57
+ * @returns Promise resolving to the number of scripts cleared
58
+ */
59
+ export async function clearScripts(windowLabel) {
60
+ await connectPlugin();
61
+ const client = getPluginClient();
62
+ const response = await client.sendCommand({
63
+ command: 'clear_scripts',
64
+ args: { windowLabel },
65
+ });
66
+ if (!response.success) {
67
+ throw new Error(response.error || 'Failed to clear scripts');
68
+ }
69
+ return response.data;
70
+ }
71
+ /**
72
+ * Gets all registered scripts.
73
+ *
74
+ * @returns Promise resolving to the list of registered scripts
75
+ */
76
+ export async function getScripts() {
77
+ await connectPlugin();
78
+ const client = getPluginClient();
79
+ const response = await client.sendCommand({
80
+ command: 'get_scripts',
81
+ args: {},
82
+ });
83
+ if (!response.success) {
84
+ throw new Error(response.error || 'Failed to get scripts');
85
+ }
86
+ return response.data;
87
+ }
88
+ /**
89
+ * Checks if a script with the given ID is registered.
90
+ *
91
+ * @param id - The script ID to check
92
+ * @returns Promise resolving to true if the script is registered
93
+ */
94
+ export async function isScriptRegistered(id) {
95
+ const { scripts } = await getScripts();
96
+ return scripts.some((s) => { return s.id === id; });
97
+ }
@@ -9,6 +9,8 @@ import { createRequire } from 'module';
9
9
  // Use createRequire to resolve the path to html2canvas in node_modules
10
10
  const require = createRequire(import.meta.url);
11
11
  let html2canvasSource = null;
12
+ /** Script ID used for the html2canvas library in the script registry. */
13
+ export const HTML2CANVAS_SCRIPT_ID = '__mcp_html2canvas__';
12
14
  /**
13
15
  * Get the html2canvas library source code.
14
16
  * Loaded lazily and cached.
@@ -21,13 +23,63 @@ export function getHtml2CanvasSource() {
21
23
  }
22
24
  return html2canvasSource;
23
25
  }
26
+ /**
27
+ * Build a script that captures a screenshot using html2canvas.
28
+ * Assumes html2canvas is already loaded (either via script manager or inline).
29
+ */
30
+ export function buildScreenshotCaptureScript(format, quality) {
31
+ // Note: This script is wrapped by executeAsyncInWebview, so we don't need an IIFE
32
+ return `
33
+ // Get the html2canvas function (may be on window, self, or globalThis)
34
+ const html2canvasFn = typeof html2canvas !== 'undefined' ? html2canvas :
35
+ (typeof window !== 'undefined' && window.html2canvas) ? window.html2canvas :
36
+ (typeof self !== 'undefined' && self.html2canvas) ? self.html2canvas :
37
+ (typeof globalThis !== 'undefined' && globalThis.html2canvas) ? globalThis.html2canvas : null;
38
+
39
+ if (!html2canvasFn) {
40
+ throw new Error('html2canvas not loaded - function not found on any global');
41
+ }
42
+
43
+ // Capture the entire document
44
+ const element = document.documentElement;
45
+ if (!element) {
46
+ throw new Error('document.documentElement is null');
47
+ }
48
+
49
+ // Configure html2canvas options
50
+ const options = {
51
+ backgroundColor: null,
52
+ scale: window.devicePixelRatio || 1,
53
+ logging: false,
54
+ useCORS: true,
55
+ allowTaint: false,
56
+ imageTimeout: 5000,
57
+ };
58
+
59
+ // Capture the webview
60
+ const canvas = await html2canvasFn(element, options);
61
+ if (!canvas) {
62
+ throw new Error('html2canvas returned null canvas');
63
+ }
64
+
65
+ // Convert to data URL
66
+ const mimeType = '${format}' === 'jpeg' ? 'image/jpeg' : 'image/png';
67
+ const dataUrl = canvas.toDataURL(mimeType, ${quality / 100});
68
+
69
+ if (!dataUrl || !dataUrl.startsWith('data:image/')) {
70
+ throw new Error('canvas.toDataURL returned invalid result: ' + (dataUrl ? dataUrl.substring(0, 50) : 'null'));
71
+ }
72
+
73
+ return dataUrl;
74
+ `;
75
+ }
24
76
  /**
25
77
  * Build a script that injects html2canvas and captures a screenshot.
78
+ * This is the legacy function that inlines the library - kept for fallback.
26
79
  */
27
80
  export function buildScreenshotScript(format, quality) {
28
81
  const html2canvas = getHtml2CanvasSource();
29
82
  // Note: This script is wrapped by executeAsyncInWebview, so we don't need an IIFE
30
- // The wrapper adds: (async () => { const scriptPromise = (async () => { ...script... })(); ... })()
31
83
  return `
32
84
  try {
33
85
  // Inject html2canvas if not already present
@@ -37,47 +89,7 @@ export function buildScreenshotScript(format, quality) {
37
89
  // After loading, html2canvas should be on globalThis/self/window
38
90
  }
39
91
 
40
- // Get the html2canvas function (may be on window, self, or globalThis)
41
- const html2canvasFn = typeof html2canvas !== 'undefined' ? html2canvas :
42
- (typeof window !== 'undefined' && window.html2canvas) ? window.html2canvas :
43
- (typeof self !== 'undefined' && self.html2canvas) ? self.html2canvas :
44
- (typeof globalThis !== 'undefined' && globalThis.html2canvas) ? globalThis.html2canvas : null;
45
-
46
- if (!html2canvasFn) {
47
- throw new Error('html2canvas failed to load - function not found on any global');
48
- }
49
-
50
- // Capture the entire document
51
- const element = document.documentElement;
52
- if (!element) {
53
- throw new Error('document.documentElement is null');
54
- }
55
-
56
- // Configure html2canvas options
57
- const options = {
58
- backgroundColor: null,
59
- scale: window.devicePixelRatio || 1,
60
- logging: false,
61
- useCORS: true,
62
- allowTaint: false,
63
- imageTimeout: 5000,
64
- };
65
-
66
- // Capture the webview
67
- const canvas = await html2canvasFn(element, options);
68
- if (!canvas) {
69
- throw new Error('html2canvas returned null canvas');
70
- }
71
-
72
- // Convert to data URL
73
- const mimeType = '${format}' === 'jpeg' ? 'image/jpeg' : 'image/png';
74
- const dataUrl = canvas.toDataURL(mimeType, ${quality / 100});
75
-
76
- if (!dataUrl || !dataUrl.startsWith('data:image/')) {
77
- throw new Error('canvas.toDataURL returned invalid result: ' + (dataUrl ? dataUrl.substring(0, 50) : 'null'));
78
- }
79
-
80
- return dataUrl;
92
+ ${buildScreenshotCaptureScript(format, quality)}
81
93
  } catch (screenshotError) {
82
94
  // Re-throw with more context
83
95
  throw new Error('Screenshot capture failed: ' + (screenshotError.message || String(screenshotError)));
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { getPluginClient, connectPlugin } from './plugin-client.js';
3
- import { buildScreenshotScript } from './scripts/html2canvas-loader.js';
3
+ import { buildScreenshotScript, buildScreenshotCaptureScript, getHtml2CanvasSource, HTML2CANVAS_SCRIPT_ID, } from './scripts/html2canvas-loader.js';
4
+ import { registerScript, isScriptRegistered } from './script-manager.js';
4
5
  /**
5
6
  * WebView Executor - Native IPC-based JavaScript execution
6
7
  *
@@ -38,16 +39,25 @@ export async function ensureReady() {
38
39
  export function resetInitialization() {
39
40
  isInitialized = false;
40
41
  }
41
- // ============================================================================
42
- // Core Execution Functions
43
- // ============================================================================
44
42
  /**
45
43
  * Execute JavaScript in the Tauri webview using native IPC via WebSocket.
46
44
  *
47
45
  * @param script - JavaScript code to execute in the webview context
48
- * @returns Result of the script execution as a string
46
+ * @param windowId - Optional window label to target (defaults to "main")
47
+ * @returns Result of the script execution with window context
48
+ */
49
+ export async function executeInWebview(script, windowId) {
50
+ const { result } = await executeInWebviewWithContext(script, windowId);
51
+ return result;
52
+ }
53
+ /**
54
+ * Execute JavaScript in the Tauri webview and return window context.
55
+ *
56
+ * @param script - JavaScript code to execute in the webview context
57
+ * @param windowId - Optional window label to target (defaults to "main")
58
+ * @returns Result of the script execution with window context
49
59
  */
50
- export async function executeInWebview(script) {
60
+ export async function executeInWebviewWithContext(script, windowId) {
51
61
  try {
52
62
  // Ensure we're fully initialized
53
63
  await ensureReady();
@@ -56,22 +66,30 @@ export async function executeInWebview(script) {
56
66
  // Use 7s timeout (longer than Rust's 5s) so errors return before Node times out.
57
67
  const response = await client.sendCommand({
58
68
  command: 'execute_js',
59
- args: { script },
69
+ args: { script, windowLabel: windowId },
60
70
  }, 7000);
61
- // console.log('executeInWebview response:', JSON.stringify(response));
62
71
  if (!response.success) {
63
72
  throw new Error(response.error || 'Unknown execution error');
64
73
  }
74
+ // Extract window context from response
75
+ const windowContext = response.windowContext;
65
76
  // Parse and return the result
66
- const result = response.data;
67
- // console.log('executeInWebview result data:', result, 'type:', typeof result);
68
- if (result === null || result === undefined) {
69
- return 'null';
77
+ const data = response.data;
78
+ let result;
79
+ if (data === null || data === undefined) {
80
+ result = 'null';
81
+ }
82
+ else if (typeof data === 'string') {
83
+ result = data;
70
84
  }
71
- if (typeof result === 'string') {
72
- return result;
85
+ else {
86
+ result = JSON.stringify(data);
73
87
  }
74
- return JSON.stringify(result);
88
+ return {
89
+ result,
90
+ windowLabel: windowContext?.windowLabel || 'main',
91
+ warning: windowContext?.warning,
92
+ };
75
93
  }
76
94
  catch (error) {
77
95
  const message = error instanceof Error ? error.message : String(error);
@@ -82,10 +100,11 @@ export async function executeInWebview(script) {
82
100
  * Execute async JavaScript in the webview with timeout support.
83
101
  *
84
102
  * @param script - JavaScript code to execute (can use await)
103
+ * @param windowId - Optional window label to target (defaults to "main")
85
104
  * @param timeout - Timeout in milliseconds (default: 5000)
86
105
  * @returns Result of the script execution
87
106
  */
88
- export async function executeAsyncInWebview(script, timeout = 5000) {
107
+ export async function executeAsyncInWebview(script, windowId, timeout = 5000) {
89
108
  const wrappedScript = `
90
109
  return (async () => {
91
110
  const timeoutPromise = new Promise((_, reject) => {
@@ -99,7 +118,7 @@ export async function executeAsyncInWebview(script, timeout = 5000) {
99
118
  return await Promise.race([scriptPromise, timeoutPromise]);
100
119
  })();
101
120
  `;
102
- return executeInWebview(wrappedScript);
121
+ return executeInWebview(wrappedScript, windowId);
103
122
  }
104
123
  // ============================================================================
105
124
  // Console Log Capture System
@@ -187,17 +206,46 @@ export async function clearConsoleLogs() {
187
206
  `;
188
207
  return executeInWebview(script);
189
208
  }
190
- // ============================================================================
191
- // Screenshot Functionality
192
- // ============================================================================
209
+ function formatScreenshotResponse(dataUrl, windowContext) {
210
+ let result = 'Webview screenshot captured (native)';
211
+ if (windowContext) {
212
+ result += ` in window "${windowContext.windowLabel}"`;
213
+ if (windowContext.warning) {
214
+ result += `\n\n⚠️ ${windowContext.warning}`;
215
+ }
216
+ }
217
+ result += `:\n\n![Screenshot](${dataUrl})`;
218
+ return result;
219
+ }
220
+ /**
221
+ * Prepares the html2canvas script for screenshot capture.
222
+ * Tries to use the script manager for persistence, falls back to inline injection.
223
+ */
224
+ async function prepareHtml2canvasScript(format, quality) {
225
+ try {
226
+ // Check if html2canvas is already registered
227
+ const isRegistered = await isScriptRegistered(HTML2CANVAS_SCRIPT_ID);
228
+ if (!isRegistered) {
229
+ // Register html2canvas via script manager for persistence across navigations
230
+ const html2canvasSource = getHtml2CanvasSource();
231
+ await registerScript(HTML2CANVAS_SCRIPT_ID, 'inline', html2canvasSource);
232
+ }
233
+ // Use the capture-only script since html2canvas is now registered
234
+ return buildScreenshotCaptureScript(format, quality);
235
+ }
236
+ catch {
237
+ // Script manager not available, fall back to inline injection
238
+ return buildScreenshotScript(format, quality);
239
+ }
240
+ }
193
241
  /**
194
242
  * Capture a screenshot of the entire webview.
195
243
  *
196
- * @param format - Image format: 'png' or 'jpeg'
197
- * @param quality - JPEG quality (0-100), only used for jpeg format
244
+ * @param options - Screenshot options (format, quality, windowId)
198
245
  * @returns Base64-encoded image data URL
199
246
  */
200
- export async function captureScreenshot(format = 'png', quality = 90) {
247
+ export async function captureScreenshot(options = {}) {
248
+ const { format = 'png', quality = 90, windowId } = options;
201
249
  // Primary implementation: Use native platform-specific APIs
202
250
  // - macOS: WKWebView takeSnapshot
203
251
  // - Windows: WebView2 CapturePreview
@@ -212,18 +260,19 @@ export async function captureScreenshot(format = 'png', quality = 90) {
212
260
  args: {
213
261
  format,
214
262
  quality,
263
+ windowLabel: windowId,
215
264
  },
216
265
  }, 15000);
217
- if (response.success && response.data) {
218
- // The native command returns a base64 data URL
219
- const dataUrl = response.data;
220
- // Validate that we got a real data URL
221
- if (dataUrl && dataUrl.startsWith('data:image/')) {
222
- return `Webview screenshot captured (native):\n\n![Screenshot](${dataUrl})`;
223
- }
266
+ if (!response.success || !response.data) {
267
+ throw new Error(response.error || 'Native screenshot returned invalid data');
268
+ }
269
+ // The native command returns a base64 data URL
270
+ const dataUrl = response.data;
271
+ if (!dataUrl || !dataUrl.startsWith('data:image/')) {
272
+ throw new Error('Native screenshot returned invalid data');
224
273
  }
225
- // If we get here, native returned but with invalid data - throw to trigger fallback
226
- throw new Error(response.error || 'Native screenshot returned invalid data');
274
+ // Build response with window context
275
+ return formatScreenshotResponse(dataUrl, response.windowContext);
227
276
  }
228
277
  catch (nativeError) {
229
278
  // Log the native error for debugging, then fall back
@@ -231,8 +280,8 @@ export async function captureScreenshot(format = 'png', quality = 90) {
231
280
  console.error(`Native screenshot failed: ${nativeMsg}, falling back to html2canvas`);
232
281
  }
233
282
  // Fallback 1: Use html2canvas library for high-quality DOM rendering
234
- // The library is bundled from node_modules, not loaded from CDN
235
- const html2canvasScript = buildScreenshotScript(format, quality);
283
+ // Try to use the script manager to register html2canvas for persistence
284
+ const html2canvasScript = await prepareHtml2canvasScript(format, quality);
236
285
  // Fallback: Try Screen Capture API if available
237
286
  // Note: This script is wrapped by executeAsyncInWebview, so we don't need an IIFE
238
287
  const screenCaptureScript = `
@@ -291,7 +340,7 @@ export async function captureScreenshot(format = 'png', quality = 90) {
291
340
  `;
292
341
  try {
293
342
  // Try html2canvas second (after native APIs)
294
- const result = await executeAsyncInWebview(html2canvasScript, 10000); // Longer timeout for library loading
343
+ const result = await executeAsyncInWebview(html2canvasScript, undefined, 10000); // Longer timeout for library loading
295
344
  // Validate that we got a real data URL, not 'null' or empty
296
345
  if (result && result !== 'null' && result.startsWith('data:image/')) {
297
346
  return `Webview screenshot captured:\n\n![Screenshot](${result})`;